package com.lee.auth.security.token;

import com.google.protobuf.InvalidProtocolBufferException;
import com.lee.auth.dto.response.AccessToken;
import com.lee.auth.security.token.TokenPayloadOuter.TokenPayload;
import com.lee.common.exception.CustomException;
import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.MACSigner;
import com.nimbusds.jose.crypto.MACVerifier;
import io.lettuce.core.SetArgs;
import io.lettuce.core.api.sync.RedisCommands;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.sql.init.dependency.AbstractBeansOfTypeDependsOnDatabaseInitializationDetector;
import org.springframework.stereotype.Component;

import java.text.ParseException;

import static com.lee.common.exception.CustomException.INVALID_CREDENTIAL_EXC;
import static com.lee.common.exception.FailureStatus.BAD_STATUS;
import static com.nimbusds.jose.JWSAlgorithm.HS256;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.time.Duration.ofDays;
import static org.springframework.util.DigestUtils.md5DigestAsHex;

/**
 * @author Lee HN
 * @date 2021/3/24 10:08
 */
@Component
public class TokenService {

    private static final String SECRET = md5DigestAsHex("lee-auth".getBytes(UTF_8));

    private static final String RE_LOGIN_PREFIX = "re_login:%s:%d";

    private static final long REFRESH_EXPIRE = ofDays(30).toSeconds();

    private final JWSHeader jwsHeader;

    private final JWSVerifier jwsVerifier;

    private final JWSSigner jwsSigner;

    private final RedisCommands<String, String> redisCommands;

    public TokenService(RedisCommands<String, String> redisCommands) throws JOSEException {
        this.jwsHeader = new JWSHeader(HS256);
        this.jwsVerifier = new MACVerifier(SECRET);
        this.jwsSigner = new MACSigner(SECRET);
        this.redisCommands = redisCommands;
    }

    /**
     * 过期时间设置两秒的宽限时间 避免请求冲突
     */
    private static final long LOOSE_TIME = 1500L;

    /**
     * 生成token
     *
     * @param payload
     * @return
     */
    public AccessToken createAccessToken(TokenPayload payload, boolean loginOrRefresh) {
        byte[] bytes = payload.toByteArray();
        JWSObject jwsObject = new JWSObject(jwsHeader, new Payload(bytes));
        try {
            jwsObject.sign(jwsSigner);
        } catch (JOSEException e) {return null;}
        String client = payload.getClient();
        long userId = payload.getId();
        long expire = payload.getExpire();
        if (loginOrRefresh)
            redisCommands.setex(tokenCacheKey(client, userId),
                    REFRESH_EXPIRE, String.valueOf(expire));
        else redisCommands.set(tokenCacheKey(client, userId),
                String.valueOf(expire), SetArgs.Builder.keepttl());
        return AccessToken.of(expire - LOOSE_TIME, jwsObject.serialize());
    }

    /**
     * 校验token
     *
     * @param accessToken
     * @return
     */
    public TokenPayload parseAccessToken(String accessToken) {
        try {
            JWSObject jwsObject = JWSObject.parse(accessToken);
            if (jwsObject.verify(jwsVerifier)) {
                byte[] bytes = jwsObject.getPayload().toBytes();
                TokenPayload tokenPayload = null;
                try {
                    tokenPayload = TokenPayload.parseFrom(bytes);
                } catch (InvalidProtocolBufferException ignored) {}
                return tokenPayload;
            }
        } catch (ParseException | JOSEException ignored) {}
        return null;
    }

    private static final CustomException REFRESH_TOO_EARLY_EXC = new CustomException(BAD_STATUS, "Refresh too early");

    /**
     * 校验token是否可用于刷新
     *
     * @return
     */
    public void verifyTokenRefresh(String clientId, long userId, long expire, long currentTime) {
        if (expire - LOOSE_TIME > currentTime)
            throw REFRESH_TOO_EARLY_EXC;
        String tokenCache = redisCommands.get(tokenCacheKey(clientId, userId));
        if (StringUtils.isBlank(tokenCache) || expire != Long.parseLong(tokenCache))
            throw INVALID_CREDENTIAL_EXC;
    }

    /**
     * 清除refreshToken
     *
     * @param userId
     */
    public void clearTokenCache(String clientId, long userId) {
        redisCommands.del(tokenCacheKey(clientId, userId));
    }

    private String tokenCacheKey(String clientId, long userId) {
        return String.format(RE_LOGIN_PREFIX, clientId, userId);
    }

}
